﻿#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include "GL/glew.h"
#include "GLFW/glfw3.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "core/core.hpp"

glm::vec3 position = glm::vec3(0, 0, 5);
glm::vec3 direction;
glm::vec3 up;
float horizontalAngle = 3.14f;
float verticalAngle = 0.0f;
float initalFoV = 45.0f;
float speed = 5.0f;
float mouseSpeed = 0.005f;
double lastTime = 0;
glm::vec3 hit;
glm::ivec3 hitPosition;

using namespace freecraft;

void computeMaricesFromInputs(GLFWwindow* window)
{

    double currentTime = glfwGetTime();
    float deltaTime = float(currentTime - lastTime);
    lastTime = currentTime;

    int width, height;
    glfwGetWindowSize(window, &width, &height);

    double xpos, ypos;
    glfwGetCursorPos(window, &xpos, &ypos);
    glfwSetCursorPos(window, width/2, height/2);

    horizontalAngle += mouseSpeed * float(width/2 - xpos);
    verticalAngle += mouseSpeed * float(height/2 - ypos);
    const float VERTICAL_ANGLE_CONSTRAINT = 1.55f;
    if (verticalAngle > VERTICAL_ANGLE_CONSTRAINT)
    {
        verticalAngle = VERTICAL_ANGLE_CONSTRAINT;
    }

    if (verticalAngle < -VERTICAL_ANGLE_CONSTRAINT)
    {
        verticalAngle = -VERTICAL_ANGLE_CONSTRAINT;
    }

    direction = glm::vec3(
        cos(verticalAngle) * sin(horizontalAngle),
        sin(verticalAngle),
        cos(verticalAngle) * cos(horizontalAngle)
    );

    glm::vec3 right = glm::vec3(
        sin(horizontalAngle - 3.14f/2.0f),
        0,
        cos(horizontalAngle - 3.14f/2.0f)
    );

    up = glm::cross(right, direction);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS){
        position += direction * deltaTime * speed;
    }
    // Move backward
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS){
        position -= direction * deltaTime * speed;
    }
    // Strafe right
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS){
        position += right * deltaTime * speed;
    }
    // Strafe left
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS){
        position -= right * deltaTime * speed;
    }

    if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS)
    {
        position += glm::vec3(0, 1, 0) * deltaTime * speed;
    }

    if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS)
    {
        position -= glm::vec3(0, 1, 0) * deltaTime * speed;
    }
}

bool drawChunk(Chunk &chunk, Shader &shader)
{
	hit = position + direction * 100.0f;
	Ray ray = Ray(position, direction);

    glm::vec3 chunkPos(chunk.position);

    for (int i=0; i<Chunk::CHUNK_SIZE; i++)
        for (int j=0; j<Chunk::CHUNK_SIZE; j++)
            for (int k=0; k<Chunk::CHUNK_SIZE; k++)
            {
                Element &element = chunk.elements[i][j][k];
                if (element.elementId == ElementId::AIR)
                    continue;

                const Object &object = Resource::getObject(element.objectId);
                if (Chunk::isSurrounded(glm::ivec3(i, j, k)))
                    continue;

                glm::mat4 trans(1.0f);
                trans = glm::translate(trans, glm::vec3(i, j, k) + chunkPos);
                shader.setMatrix("transform", trans);

                glBindVertexArray(object.vao);
                glDrawElements(GL_TRIANGLES, object.indices.size(), GL_UNSIGNED_SHORT, 0);

				glm::vec3 h;
				glm::vec3 offset(i, j, k);
				if (ray.rayCheck(object.aabb.min + offset, object.aabb.max + offset, h))
				{
					float dist = glm::distance(h, position);
					if (dist < glm::distance(hit, position))
					{
						hit = h;
						hitPosition = glm::ivec3(i, j, k);
					}
				}
            }

    return true;
}

void drawBorder(const Object &object)
{
    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
    glStencilMask(0x00); // 禁止写入模板缓冲区
    glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
    glDisable(GL_DEPTH_TEST);

    glDrawElements(GL_TRIANGLES, object.indices.size(), GL_UNSIGNED_SHORT, 0);

    glEnable(GL_DEPTH_TEST);
    glStencilMask(0xff);
}

GLuint generateFrontSight()
{
    const float frontSightVertex[] = 
    {
        0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
        0.1f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.1f, 0.0f, 0.0f, 1.0f, 1.0f,
        0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 0.1f, 0.0f, 0.0f, 1.0f,
    };

    const unsigned short frontSightIndices[] = 
    {
        0, 1, 
        2, 3, 
        4, 5,
    };

    GLuint vao, vbo, ebo;
    glGenVertexArrays(1, &vao);
    glGenBuffers(1, &vbo);
    glGenBuffers(1, &ebo);

    glBindVertexArray(vao);

    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(frontSightVertex), frontSightVertex, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frontSightIndices), frontSightIndices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);

    return vao;
}

void drawFrontSight(Shader &shader, GLuint vao)
{
    glm::mat4 MVP;
    glm::mat4 projection = glm::ortho(-1, 1, -1, 1);
    glm::mat4 view = glm::lookAt(
        -direction * 0.5f,
        glm::vec3(0, 0, 0),
        up
        );
    glm::mat4 model = glm::mat4(1.0f);
    MVP = projection * view * model;

    shader.use();

    shader.setMatrix("MVP", MVP);
    shader.setMatrix("transform", glm::mat4(1.0f));

    glDisable(GL_DEPTH_TEST);
    glDisable(GL_STENCIL_TEST);

    glBindVertexArray(vao);
    glDrawElements(GL_LINES, 6, GL_UNSIGNED_SHORT, 0);

    glEnable(GL_DEPTH_TEST);
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
    if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE)
    {
        glfwSetWindowShouldClose(window, GLFW_TRUE);
    }
}

void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
    if ((button == GLFW_MOUSE_BUTTON_LEFT) && (action == GLFW_PRESS))
    {
        Element *element;
        if ((element = Chunk::getElement(hitPosition)) != nullptr)
        {
            element->elementId = ElementId::AIR;
        }
    }

    if ((button == GLFW_MOUSE_BUTTON_RIGHT) && (action == GLFW_PRESS))
    {
        Element* element;
        glm::vec3 tempHit = (hit - glm::vec3(hitPosition)) / 0.5f;
        glm::ivec3 pos(tempHit.x, tempHit.y, tempHit.z);

        if ((element = Chunk::getElement(pos + hitPosition)) != nullptr)
        {
            element->elementId = ElementId::BASE;
            element->objectId = ObjectId::BASE;
        }
    }
}

int main(int argc, char* argv[])
{
    GLFWwindow* window;

    if (!glfwInit())
        return -1;

    window = glfwCreateWindow(1024, 768, "freecraft", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    int width, height;
    glfwGetWindowSize(window, &width, &height);
    glfwSetCursorPos(window, width/2, height/2);

    glfwMakeContextCurrent(window);
    glfwSetKeyCallback(window, key_callback);
    glfwSetMouseButtonCallback(window, mouse_button_callback);
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    glewInit();

    glm::mat4   MVP;

    glClearColor(0.0f, 0.0f, 0.4f, 0.0f);
    
    glEnable(GL_DEPTH_TEST);
	glEnable(GL_CULL_FACE);
    glDepthFunc(GL_LESS);

    Shader baseShader(
        "shader/SimpleVertexShader.vertexshader", 
        "shader/SimpleFragmentShader.fragmentshader"
    );

    Shader borderShader("shader/border.vs", "shader/border.fs");
    Shader frontSightShader("shader/front-sight.vs", "shader/front-sight.fs");

    GLuint frontSightVAO = generateFrontSight();

    Chunk *chunk = new Chunk(glm::ivec3(0, 0, 0));

    Texture texture;
    Texture::loadTexture("resource/brick.png", texture);

    int frameCounter = 0;
    double frameLastTime = glfwGetTime();
    while (!glfwWindowShouldClose(window))
    {
        frameCounter++;
        double frameCurrentTime = glfwGetTime();
        if ((frameCurrentTime - frameLastTime) >= 1.0)
        {
            std::cout << 1000.0 / double(frameCounter) << " ms/frame\n";
            frameCounter = 0;
            frameLastTime += 1.0;
        }

		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);


        computeMaricesFromInputs(window);

        baseShader.use();

        glm::mat4 projection = glm::perspective(glm::radians(initalFoV), 4.0f/3.0f, 0.1f, 100.0f);
        glm::mat4 view = glm::lookAt(
            position,
            position + direction,
            up
            );
        glm::mat4 model = glm::mat4(1.0f);
        MVP = projection * view * model;

        baseShader.setMatrix("MVP", MVP);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture.id);    

        drawChunk(*chunk, baseShader);

        do 
        {
            int x = (int)hitPosition.x;
            int y = (int)hitPosition.y;
            int z = (int)hitPosition.z;
            if (!((x < Chunk::CHUNK_SIZE) && (y < Chunk::CHUNK_SIZE) && (z < Chunk::CHUNK_SIZE) &&
                        (x >= 0) && (y >= 0) && (z >= 0)))
                break;

            if (chunk->elements[x][y][z].elementId == ElementId::AIR)
                break;

            const Object &object = Resource::getObject(chunk->elements[x][y][z].objectId);

            glEnable(GL_STENCIL_TEST);
			glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
            glStencilMask(0xff);
            glStencilFunc(GL_ALWAYS, 1, 0xFF);
            // 在模板测试和深度测试都通过时更新模板缓冲区
            glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); 
			glDisable(GL_DEPTH_TEST);

            glBindVertexArray(object.vao);

			glm::mat4 trans(1.0f);
			trans = glm::translate(trans, glm::vec3(x, y, z));
			baseShader.setMatrix("transform", trans);

            glDrawElements(GL_TRIANGLES, object.indices.size(), GL_UNSIGNED_SHORT, 0);

            glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

            trans = glm::mat4(1.0f);
			trans = glm::translate(trans, glm::vec3(x, y, z));
            trans = glm::scale(trans, glm::vec3(1.05f));
            borderShader.use();
            borderShader.setMatrix("MVP", MVP);
            borderShader.setMatrix("transform", trans);

            drawBorder(object);
        }while (0);

        drawFrontSight(frontSightShader, frontSightVAO);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();

    return 0;
}
